package org.example.nio;

import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import okhttp3.*;
import org.example.nio.filter.HttpRequestFilter;
import org.example.nio.filter.HttpResponseFilter;
import org.example.nio.filter.MyRequestFilter;
import org.example.nio.filter.MyResponseFilter;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class OkHttpProxyHandler implements ProxyHandler {
    private final OkHttpClient okHttpClient = new OkHttpClient();
    private final ExecutorService executorService;
    private final String proxyServer;
    private final HttpResponseFilter responseFilter = new MyResponseFilter();
    private final HttpRequestFilter requestFilter = new MyRequestFilter();
    private final boolean syncMode;

    public OkHttpProxyHandler(String proxyServer, boolean syncMode) {
        this.proxyServer = proxyServer;
        this.syncMode = syncMode;
        executorService = new ThreadPoolExecutor(8, 8,
                1000, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(2048),
                new ThreadPoolExecutor.CallerRunsPolicy());
    }

    @Override
    public void handle(FullHttpRequest request, ChannelHandlerContext ctx) {
        requestFilter.filter(request);
        if (request.method() == HttpMethod.GET) {
            executorService.submit(() -> processGet(request, ctx));
        } else {
            // unsupported
            ctx.close();
        }
    }

    private void processGet(FullHttpRequest request, ChannelHandlerContext ctx) {
        HttpHeaders headers = request.headers();
        Headers.Builder proxyHeadersBuilder = new Headers.Builder();

        Iterator<Map.Entry<String, String>> iterators = headers.iteratorAsString();
        while (iterators.hasNext()) {
            Map.Entry<String, String> it = iterators.next();
            proxyHeadersBuilder.add(it.getKey(), it.getValue());
        }

        Request proxyRequest = new Request.Builder()
                .url(proxyServer + request.uri())
                .headers(proxyHeadersBuilder.build())
                .get()
                .build();

        if (syncMode) {
            syncGet(request, ctx, proxyRequest);
        } else {
            asyncGet(request, ctx, proxyRequest);
        }
    }

    private void processResponse(FullHttpRequest request, ChannelHandlerContext ctx, Response proxyResponse) {
        FullHttpResponse response = null;
        try {
            HttpVersion httpVersion = HttpVersion.valueOf(proxyResponse.protocol().toString());
            HttpResponseStatus httpResponseStatus = HttpResponseStatus.valueOf(proxyResponse.code());
            response = new DefaultFullHttpResponse(httpVersion, httpResponseStatus);
            HttpHeaders responseHeaders = response.headers();
            Headers proxyResponseHeaders = proxyResponse.headers();
            for (String headerName : proxyResponseHeaders.names()) {
                responseHeaders.set(headerName, proxyResponseHeaders.get(headerName));
            }
            if (proxyResponse.body() != null) {
                response.content().writeBytes(proxyResponse.body().bytes());
            }
            responseFilter.filter(response);
        } catch (IOException ex) {
            ex.printStackTrace();
            ctx.close();
        } finally {
            if (response != null) {
                if (HttpUtil.isKeepAlive(request)) {
                    ctx.write(response).addListener(ChannelFutureListener.CLOSE);
                } else {
                    ctx.write(response);
                }
            }
            ctx.flush();
        }
    }

    private void asyncGet(FullHttpRequest request, ChannelHandlerContext ctx, Request proxyRequest) {
        okHttpClient.newCall(proxyRequest).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
                ctx.close();
            }

            @Override
            public void onResponse(Call call, Response proxyResponse) {
                processResponse(request, ctx, proxyResponse);
            }
        });
    }

    private void syncGet(FullHttpRequest request, ChannelHandlerContext ctx, Request proxyRequest) {
        try (Response proxyResponse = okHttpClient.newCall(proxyRequest).execute()) {
            processResponse(request, ctx, proxyResponse);
        } catch (IOException ex) {
            ex.printStackTrace();
            ctx.close();
        }
    }
}
